﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Xceed.Silverlight.Data;

namespace Xceed.Silverlight.DataGrid
{
  public class SelectionRange
  {
    public SelectionRange()
    {
    }

    public SelectionRange( object selectedItem )
      : this()
    {
      this.StartRangeInfos.FromItem( selectedItem );
      this.EndRangeInfos.ToItem( selectedItem );
    }

    public SelectionRange( FilterExpressionBase filterExpression, Predicate<object> filter )
      : this()
    {
      if( ( filterExpression == null ) && ( filter == null ) )
        throw new InvalidOperationException( "An attempt was made to create a selection range while both the filterExpression and filter parameters are null." );

      m_sortDescriptions = new List<SortDescription>();
      this.SortDescriptions = new ReadOnlyCollection<SortDescription>( m_sortDescriptions );

      m_filterExpression = filterExpression;
      m_filter = filter;

      this.StartRangeInfos.FromStart();
      this.EndRangeInfos.ToEnd();

      this.UseDefaultDataRangeContext = false;
    }

    public SelectionRange( IEnumerable<SortDescription> sortDescriptons, FilterExpressionBase filterExpression, Predicate<object> filter )
      : this()
    {
      if( sortDescriptons == null )
        throw new ArgumentNullException( "sortDescriptons" );

      m_sortDescriptions = new List<SortDescription>( sortDescriptons );
      this.SortDescriptions = new ReadOnlyCollection<SortDescription>( m_sortDescriptions );

      //both these properties CAN be null.
      m_filterExpression = filterExpression;
      m_filter = filter;

      this.UseDefaultDataRangeContext = false;
    }

    private bool IsStartRangeBeforeEndRange()
    {
      if( this.StartRangeInfos.SelectFromStart == true )
        return true;
      if( this.EndRangeInfos.SelectToEnd == true )
        return true;

      int compare = DataRange.CompareHelper( this.StartRangeInfos, this.StartRangeInfos.IsInclusive, this.EndRangeInfos, this.EndRangeInfos.IsInclusive, this.SortDescriptions );
      return compare < 0;
    }

    internal IDictionary<String, object> GetLowerBound( out bool isLowerBoundInclusive )
    {
      bool isStartBeforeEnd = this.IsStartRangeBeforeEndRange();
      if( isStartBeforeEnd )
      {
        isLowerBoundInclusive = this.StartRangeInfos.IsInclusive;
        return ( IDictionary<String, object> )this.StartRangeInfos;
      }

      isLowerBoundInclusive = this.EndRangeInfos.IsInclusive;
      return ( IDictionary<String, object> )this.EndRangeInfos;
    }

    internal IDictionary<String, object> GetHigherBound( out bool isHigherBoundInclusive )
    {
      bool isStartBeforeEnd = this.IsStartRangeBeforeEndRange();

      if( isStartBeforeEnd )
      {
        isHigherBoundInclusive = this.EndRangeInfos.IsInclusive;
        return ( IDictionary<String, object> )this.EndRangeInfos;
      }

      isHigherBoundInclusive = this.StartRangeInfos.IsInclusive;
      return ( IDictionary<String, object> )this.StartRangeInfos;
    }

    internal bool IsAllBoundSortsContainedInOtherBoundSorts( bool isLowerBound )
    {
      IDictionary<String, object> bound;
      bool isBoundInclusive;

      if( isLowerBound )
      {
        bound = this.GetLowerBound( out isBoundInclusive );
      }
      else
      {
        bound = this.GetHigherBound( out isBoundInclusive );
      }

      return IsAllBoundSortsContainedInOtherBoundSorts( bound );
    }

    internal bool IsAllBoundSortsContainedInOtherBoundSorts( IDictionary<String, object> rangeBound )
    {
      IDictionary<String, object> otherBound;
      if( ( IDictionary<String, object> )this.StartRangeInfos == rangeBound )
      {
        otherBound = ( IDictionary<String, object> )this.EndRangeInfos;
      }
      else if( ( IDictionary<String, object> )this.EndRangeInfos == rangeBound )
      {
        otherBound = ( IDictionary<String, object> )this.StartRangeInfos;
      }
      else
      {
        throw new InvalidOperationException( "An attempt was made to use a range with bounds that are not part of the selection range." );
      }

      for( int i = 0; i < rangeBound.Values.Count; ++i )
      {
        string propName = SortDescriptions[ i ].PropertyName;
        object rangeBoundKeyValue;
        object otherRangeBoundKeyValue;

        rangeBound.TryGetValue( propName, out rangeBoundKeyValue );
        otherBound.TryGetValue( propName, out otherRangeBoundKeyValue );

        if( rangeBoundKeyValue != otherRangeBoundKeyValue )
          return false;
      }

      return true;
    }

    private void OnRangeChanging( object sender, EventArgs e )
    {
      throw new InvalidOperationException( "An attempt was made to change the StartRangeInfo or EndRangeInfo properties of a SelectionRange that is selecting from the start or until the end, respectively." );
    }

    public static SelectionRange All
    {
      get
      {
        SelectionRange selectAll = new SelectionRange();
        selectAll.StartRangeInfos.FromStart();
        selectAll.EndRangeInfos.ToEnd();

        return selectAll;
      }
    }

    #region SortDescriptions

    public ReadOnlyCollection<SortDescription> SortDescriptions
    {
      get;
      private set;
    }

    private IList<SortDescription> m_sortDescriptions;

    #endregion

    #region SelectionType

    public SelectionType SelectionType
    {
      get
      {
        return m_selectionType;
      }
      set
      {
        m_selectionType = value;
      }
    }

    private SelectionType m_selectionType = SelectionType.Selection;

    #endregion

    #region FilterExpression Property

    public FilterExpressionBase FilterExpression
    {
      get
      {
        return m_filterExpression;
      }
      private set
      {
        if( value != m_filterExpression )
        {
          m_filterExpression = value;
          m_filterExpressionPredicate = null;
        }
      }
    }

    private FilterExpressionBase m_filterExpression; //null
    private Func<object, bool> m_filterExpressionPredicate; //null

    #endregion

    #region Filter Property

    public Predicate<object> Filter
    {
      get
      {
        return m_filter;
      }
      private set
      {
        if( value != m_filter )
        {
          m_filter = value;
        }
      }
    }

    private Predicate<object> m_filter;

    #endregion

    #region StartRangeInfos Property

    public StartRangeDictionary StartRangeInfos
    {
      get
      {
        return m_startRange;
      }
    }

    private StartRangeDictionary m_startRange = new StartRangeDictionary();

    #endregion StartRangeInfos Property

    #region EndRangeInfos Property

    public EndRangeDictionary EndRangeInfos
    {
      get
      {
        return m_endRange;
      }
    }

    private EndRangeDictionary m_endRange = new EndRangeDictionary();

    #endregion EndRangeInfos Property

    #region IsEmpty Property

    public bool IsEmpty
    {
      get
      {
        if( ( this.SortDescriptions.Count <= 1 ) && ( this.StartRangeInfos.Count == 0 ) && ( this.EndRangeInfos.Count == 0 )
          && ( this.SelectFromStart == null ) && ( this.SelectToEnd == null )
          && ( this.StartRangeInfos.GetFromItem() == null ) && ( this.EndRangeInfos.GetToItem() == null ) )
          return true;

        return false;
      }
    }

    #endregion IsEmpty Property

    #region HaveStartLimit Property

    internal bool HaveStartLimit
    {
      get
      {
        if( ( this.StartRangeInfos.Count > 0 ) || ( this.SelectFromStart == true )
          || ( this.StartRangeInfos.GetFromItem() != null ) )
          return true;

        return false;
      }
    }

    #endregion HaveEndLimit Property

    #region HaveEndLimit Property

    internal bool HaveEndLimit
    {
      get
      {
        if( ( this.EndRangeInfos.Count > 0 ) || ( this.SelectToEnd == true )
          || ( this.EndRangeInfos.GetToItem() != null ) )
          return true;

        return false;
      }
    }

    #endregion HaveEndLimit Property

    #region IsSingleItem Property

    internal bool IsSingleItem
    {
      get
      {
        if( this.SortDescriptions != null )
        {
          return false;
        }
        else
        {
          object fromItem = this.StartRangeInfos.GetFromItem();
          object toItem = this.EndRangeInfos.GetToItem();

          return ( fromItem != null )
            && object.Equals( fromItem, toItem )
            && ( this.StartRangeInfos.IsInclusive == this.EndRangeInfos.IsInclusive );
        }
      }
    }

    #endregion

    #region UseDefaultDataRangeContext Property

    internal bool UseDefaultDataRangeContext
    {
      get
      {
        return m_useDefaultDataRangeContext;
      }
      private set
      {
        if( m_useDefaultDataRangeContext != value )
        {
          m_useDefaultDataRangeContext = value;
        }
      }
    }

    private bool m_useDefaultDataRangeContext = true;

    #endregion

    #region SelectFromStart Property

    internal Nullable<bool> SelectFromStart
    {
      get
      {
        return m_startRange.SelectFromStart;
      }
    }

    #endregion

    #region SelectToEnd

    internal Nullable<bool> SelectToEnd
    {
      get
      {
        return m_endRange.SelectToEnd;
      }
    }

    #endregion

    #region DataRangeContext Property

    internal DataRangeContext DataRangeContext
    {
      get
      {
        return m_dataRangeContext;
      }
      set
      {
        if( value != m_dataRangeContext )
        {
          if( m_dataRangeContext != null )
            throw new InvalidOperationException( "An attempt was made to set a DataRangeContext when one has already been provided." );

          m_dataRangeContext = value;

          if( m_dataRangeContext != null )
          {
            m_filter = m_dataRangeContext.Filter;
            m_filterExpression = m_dataRangeContext.FilterExpression;

            m_sortDescriptions = m_dataRangeContext.SortDescriptions;
            this.SortDescriptions = new ReadOnlyCollection<SortDescription>( m_sortDescriptions );
          }
        }
      }
    }

    private DataRangeContext m_dataRangeContext;

    #endregion

    public override bool Equals( object obj )
    {
      if( !( obj is SelectionRange ) )
        return false;

      SelectionRange selectionRange = ( SelectionRange )obj;

      return this.Equals( selectionRange );
    }

    public bool Equals( SelectionRange obj )
    {
      if( obj == null )
        return false;

      // Only one of the compared ranges have SelectFromStart or SelectFromEnd => different SelectionRanges.
      if( ( this.SelectFromStart != obj.SelectFromStart ) || ( this.SelectToEnd != obj.SelectToEnd ) )
        return false;

      // Both have same values for SelectFromStart and SelectFromEnd and they both are true, same SelectionRange.
      if( ( this.SelectFromStart == true ) && ( this.SelectToEnd == true ) )
        return true;

      // Only one of the compared ranges have FromItem or ToItem => different SelectionRanges.
      if( ( this.StartRangeInfos.GetFromItem() != obj.StartRangeInfos.GetFromItem() ) || ( this.EndRangeInfos.GetToItem() != obj.EndRangeInfos.GetToItem() ) )
        return false;

      // Both have same values for FromItem and ToItem and they both are not null, same SelectionRange.
      if( ( this.StartRangeInfos.GetFromItem() != null ) && ( this.EndRangeInfos.GetToItem() != null )
        && ( this.StartRangeInfos.GetFromItem() == obj.StartRangeInfos.GetFromItem() )
        && ( this.EndRangeInfos.GetToItem() == obj.EndRangeInfos.GetToItem() ) )
        return true;

      if( ( this.StartRangeInfos.IsInclusive != obj.StartRangeInfos.IsInclusive ) || ( this.EndRangeInfos.IsInclusive != obj.EndRangeInfos.IsInclusive ) )
        return false;

      int count = this.SortDescriptions.Count;

      if( count != obj.SortDescriptions.Count )
        return false;

      for( int i = 0; i < count; i++ )
      {
        SortDescription sortDesc = this.SortDescriptions[ i ];

        if( !sortDesc.Equals( obj.SortDescriptions[ i ] ) )
          return false;

        try
        {
          // Only compare start value if not from start because StartRangeInfo
          // does not contain any value
          if( this.SelectFromStart == false )
          {
            object startValue1 = this.StartRangeInfos[ sortDesc.PropertyName ];
            object startValue2 = obj.StartRangeInfos[ sortDesc.PropertyName ];

            if( !startValue1.Equals( startValue2 ) )
              return false;
          }

          // Only compare end value if not from end because EndRangeInfo
          // does not contain any value
          if( this.SelectToEnd == false )
          {
            object endValue1 = this.EndRangeInfos[ sortDesc.PropertyName ];
            object endValue2 = obj.EndRangeInfos[ sortDesc.PropertyName ];

            if( !endValue1.Equals( endValue2 ) )
              return false;
          }
        }
        catch( KeyNotFoundException )
        {
          throw new InvalidOperationException( "The data in the SelectionRange is not consistent." );
        }
      }

      return true;
    }

    public override int GetHashCode()
    {
      int hash = 17;

      foreach( SortDescription sortDesc in this.SortDescriptions )
      {
        hash = hash ^ sortDesc.GetHashCode();

        object outValue;

        if( !this.StartRangeInfos.TryGetValue( sortDesc.PropertyName, out outValue ) )
        {
          throw new InvalidOperationException( "The data in the SelectionRange is not consistent." );
        }
        else
        {
          hash = hash ^ outValue.GetHashCode();
        }

        if( !this.EndRangeInfos.TryGetValue( sortDesc.PropertyName, out outValue ) )
        {
          throw new InvalidOperationException( "The data in the SelectionRange is not consistent." );
        }
        else
        {
          hash = hash ^ outValue.GetHashCode();
        }
      }

      return hash;
    }

    internal bool IsFilteredOut( object item )
    {
      if( this.Filter != null && !this.Filter( item ) )
        return true;

      if( this.FilterExpression != null )
      {
        if( m_filterExpressionPredicate == null )
        {
          PredicateBuilder pb = new PredicateBuilder( this.FilterExpression );

          m_filterExpressionPredicate = pb.ToPredicate( item.GetType() );
        }

        if( !m_filterExpressionPredicate.Invoke( item ) )
          return true;
      }

      return false;
    }

    #region Sub Classes

    public class StartRangeDictionary : NotificationDictionary<string, object>
    {
      public StartRangeDictionary()
      {
        this.IsInclusive = true;
        this.DictionaryChanging += new EventHandler( OnStartRangeDictionaryChanging );
      }

      public bool IsInclusive
      {
        get;
        set;
      }

      internal Nullable<bool> SelectFromStart
      {
        get;
        private set;
      }

      public void FromStart()
      {
        if( this.SelectFromStart != true )
        {
          if( this.Count > 0 )
          {
            this.Clear();
          }
          this.SelectFromStart = true;
          this.FromItem( null );
        }
      }

      #region FromItem

      public void FromItem( object item )
      {
        m_fromItem = item;
        if( m_fromItem != null )
        {
          this.SelectFromStart = false;
        }
      }

      internal object GetFromItem()
      {
        return m_fromItem;
      }

      private object m_fromItem; //null

      #endregion

      private void OnStartRangeDictionaryChanging( object sender, EventArgs e )
      {
        if( this.SelectFromStart != false )
        {
          this.SelectFromStart = false;
        }
      }
    }

    public class EndRangeDictionary : NotificationDictionary<string, object>
    {
      public EndRangeDictionary()
      {
        this.IsInclusive = true;
        this.DictionaryChanging += new EventHandler( OnEndRangeDictionaryChanging );
      }

      public bool IsInclusive
      {
        get;
        set;
      }

      internal Nullable<bool> SelectToEnd
      {
        get;
        private set;
      }

      public void ToEnd()
      {
        if( this.SelectToEnd != true )
        {
          if( this.Count > 0 )
          {
            this.Clear();
          }
          this.SelectToEnd = true;
          this.ToItem( null );
        }
      }

      #region ToItem

      public void ToItem( object item )
      {
        m_toItem = item;
        if( m_toItem != null )
        {
          this.SelectToEnd = false;
        }
      }

      internal object GetToItem()
      {
        return m_toItem;
      }

      private object m_toItem; //null

      #endregion

      private void OnEndRangeDictionaryChanging( object sender, EventArgs e )
      {
        if( this.SelectToEnd != false )
        {
          this.SelectToEnd = false;
        }
      }
    }

    public class NotificationDictionary<TKey, TValue> : IDictionary<TKey, TValue>
    {
      public event EventHandler DictionaryChanging;
      public event EventHandler DictionaryChanged;

      #region IDictionary<TKey,TValue> Members

      public void Add( TKey key, TValue value )
      {
        this.OnDictionaryChanging();
        m_wrappedDictionary.Add( key, value );
        this.OnDictionaryChanged();
      }

      public bool ContainsKey( TKey key )
      {
        return m_wrappedDictionary.ContainsKey( key );
      }

      public ICollection<TKey> Keys
      {
        get
        {
          return m_wrappedDictionary.Keys;
        }
      }

      public bool Remove( TKey key )
      {
        this.OnDictionaryChanging();
        bool retval = m_wrappedDictionary.Remove( key );
        this.OnDictionaryChanged();

        return retval;
      }

      public bool TryGetValue( TKey key, out TValue value )
      {
        return m_wrappedDictionary.TryGetValue( key, out value );
      }

      public ICollection<TValue> Values
      {
        get
        {
          return m_wrappedDictionary.Values;
        }
      }

      public TValue this[ TKey key ]
      {
        get
        {
          return m_wrappedDictionary[ key ];
        }
        set
        {
          this.OnDictionaryChanging();
          m_wrappedDictionary[ key ] = value;
          this.OnDictionaryChanged();
        }
      }

      #endregion

      #region ICollection<KeyValuePair<TKey,TValue>> Members

      public void Add( KeyValuePair<TKey, TValue> item )
      {
        this.OnDictionaryChanging();
        ( ( ICollection<KeyValuePair<TKey, TValue>> )m_wrappedDictionary ).Add( item );
        this.OnDictionaryChanged();
      }

      public void Clear()
      {
        this.OnDictionaryChanging();
        m_wrappedDictionary.Clear();
        this.OnDictionaryChanged();
      }

      public bool Contains( KeyValuePair<TKey, TValue> item )
      {
        return ( ( ICollection<KeyValuePair<TKey, TValue>> )m_wrappedDictionary ).Contains( item );
      }

      public void CopyTo( KeyValuePair<TKey, TValue>[] array, int arrayIndex )
      {
        ( ( ICollection<KeyValuePair<TKey, TValue>> )m_wrappedDictionary ).CopyTo( array, arrayIndex );
      }

      public int Count
      {
        get
        {
          return m_wrappedDictionary.Count;
        }
      }

      public bool IsReadOnly
      {
        get
        {
          return ( ( ICollection<KeyValuePair<TKey, TValue>> )m_wrappedDictionary ).IsReadOnly;
        }
      }

      public bool Remove( KeyValuePair<TKey, TValue> item )
      {
        this.OnDictionaryChanging();

        bool retval = ( ( ICollection<KeyValuePair<TKey, TValue>> )m_wrappedDictionary ).Remove( item );

        this.OnDictionaryChanged();

        return retval;
      }

      #endregion

      #region IEnumerable<KeyValuePair<TKey,TValue>> Members

      public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
      {
        return ( ( IEnumerable<KeyValuePair<TKey, TValue>> )m_wrappedDictionary ).GetEnumerator();
      }

      #endregion

      #region IEnumerable Members

      IEnumerator IEnumerable.GetEnumerator()
      {
        return ( ( IEnumerable )m_wrappedDictionary ).GetEnumerator();
      }

      #endregion

      protected void OnDictionaryChanging()
      {
        if( this.DictionaryChanging != null )
        {
          this.DictionaryChanging( this, EventArgs.Empty );
        }
      }

      protected void OnDictionaryChanged()
      {
        if( this.DictionaryChanged != null )
        {
          this.DictionaryChanged( this, EventArgs.Empty );
        }
      }

      private Dictionary<TKey, TValue> m_wrappedDictionary = new Dictionary<TKey, TValue>();
    }

    #endregion
  }
}
